"""Sensor platform for Miele integration."""

from __future__ import annotations

from collections.abc import Callable, Mapping
from dataclasses import dataclass
from datetime import datetime, timedelta
import logging
from typing import Any, Final, cast

from pymiele import MieleDevice, MieleTemperature

from homeassistant.components.sensor import (
    RestoreSensor,
    SensorDeviceClass,
    SensorEntity,
    SensorEntityDescription,
    SensorStateClass,
)
from homeassistant.const import (
    PERCENTAGE,
    REVOLUTIONS_PER_MINUTE,
    EntityCategory,
    UnitOfEnergy,
    UnitOfTemperature,
    UnitOfTime,
    UnitOfVolume,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.util import dt as dt_util

from .const import (
    COFFEE_SYSTEM_PROFILE,
    DISABLED_TEMP_ENTITIES,
    DOMAIN,
    PROGRAM_IDS,
    PROGRAM_PHASE,
    STATE_STATUS_TAGS,
    MieleAppliance,
    PlatePowerStep,
    StateDryingStep,
    StateProgramType,
    StateStatus,
)
from .coordinator import MieleConfigEntry, MieleDataUpdateCoordinator
from .entity import MieleEntity

PARALLEL_UPDATES = 0

_LOGGER = logging.getLogger(__name__)

DEFAULT_PLATE_COUNT = 4

PLATE_COUNT = {
    "KM7575": 6,
    "KM7678": 6,
    "KM7697": 6,
    "KM7878": 6,
    "KM7897": 6,
    "KMDA7633": 5,
    "KMDA7634": 5,
    "KMDA7774": 5,
    "KMX": 6,
}

ATTRIBUTE_PROFILE = "profile"


def _get_plate_count(tech_type: str) -> int:
    """Get number of zones for hob."""
    stripped = tech_type.replace(" ", "")
    for prefix, plates in PLATE_COUNT.items():
        if stripped.startswith(prefix):
            return plates
    return DEFAULT_PLATE_COUNT


def _convert_duration(value_list: list[int]) -> int | None:
    """Convert duration to minutes."""
    return value_list[0] * 60 + value_list[1] if value_list else None


def _convert_temperature(
    value_list: list[MieleTemperature], index: int
) -> float | None:
    """Convert temperature object to readable value."""
    if index >= len(value_list):
        return None
    raw_value = cast(int, value_list[index].temperature) / 100.0
    if raw_value in DISABLED_TEMP_ENTITIES:
        return None
    return raw_value


def _get_coffee_profile(value: MieleDevice) -> str | None:
    """Get coffee profile from value."""
    if value.state_program_id is not None:
        for key_range, profile in COFFEE_SYSTEM_PROFILE.items():
            if value.state_program_id in key_range:
                return profile
    return None


def _convert_start_timestamp(
    elapsed_time_list: list[int], start_time_list: list[int]
) -> datetime | None:
    """Convert raw values representing time into start timestamp."""
    now = dt_util.utcnow()
    elapsed_duration = _convert_duration(elapsed_time_list)
    delayed_start_duration = _convert_duration(start_time_list)
    if (elapsed_duration is None or elapsed_duration == 0) and (
        delayed_start_duration is None or delayed_start_duration == 0
    ):
        return None
    if elapsed_duration is not None and elapsed_duration > 0:
        duration = -elapsed_duration
    elif delayed_start_duration is not None and delayed_start_duration > 0:
        duration = delayed_start_duration
    delta = timedelta(minutes=duration)
    return (now + delta).replace(second=0, microsecond=0)


def _convert_finish_timestamp(
    remaining_time_list: list[int], start_time_list: list[int]
) -> datetime | None:
    """Convert raw values representing time into finish timestamp."""
    now = dt_util.utcnow()
    program_duration = _convert_duration(remaining_time_list)
    delayed_start_duration = _convert_duration(start_time_list)
    if program_duration is None or program_duration == 0:
        return None
    duration = program_duration + (
        delayed_start_duration if delayed_start_duration is not None else 0
    )
    delta = timedelta(minutes=duration)
    return (now + delta).replace(second=0, microsecond=0)


@dataclass(frozen=True, kw_only=True)
class MieleSensorDescription(SensorEntityDescription):
    """Class describing Miele sensor entities."""

    value_fn: Callable[[MieleDevice], StateType | datetime]
    end_value_fn: Callable[[StateType | datetime], StateType | datetime] | None = None
    extra_attributes: dict[str, Callable[[MieleDevice], StateType]] | None = None
    zone: int | None = None
    unique_id_fn: Callable[[str, MieleSensorDescription], str] | None = None


@dataclass
class MieleSensorDefinition:
    """Class for defining sensor entities."""

    types: tuple[MieleAppliance, ...]
    description: MieleSensorDescription


SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = (
    MieleSensorDefinition(
        types=(
            MieleAppliance.WASHING_MACHINE,
            MieleAppliance.WASHING_MACHINE_SEMI_PROFESSIONAL,
            MieleAppliance.TUMBLE_DRYER,
            MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL,
            MieleAppliance.DISHWASHER,
            MieleAppliance.OVEN,
            MieleAppliance.OVEN_MICROWAVE,
            MieleAppliance.HOB_HIGHLIGHT,
            MieleAppliance.STEAM_OVEN,
            MieleAppliance.MICROWAVE,
            MieleAppliance.COFFEE_SYSTEM,
            MieleAppliance.HOOD,
            MieleAppliance.FRIDGE,
            MieleAppliance.FREEZER,
            MieleAppliance.FRIDGE_FREEZER,
            MieleAppliance.ROBOT_VACUUM_CLEANER,
            MieleAppliance.WASHER_DRYER,
            MieleAppliance.DISH_WARMER,
            MieleAppliance.HOB_INDUCTION,
            MieleAppliance.STEAM_OVEN_COMBI,
            MieleAppliance.WINE_CABINET,
            MieleAppliance.WINE_CONDITIONING_UNIT,
            MieleAppliance.WINE_STORAGE_CONDITIONING_UNIT,
            MieleAppliance.STEAM_OVEN_MICRO,
            MieleAppliance.DIALOG_OVEN,
            MieleAppliance.WINE_CABINET_FREEZER,
            MieleAppliance.STEAM_OVEN_MK2,
            MieleAppliance.HOB_INDUCT_EXTR,
        ),
        description=MieleSensorDescription(
            key="state_status",
            translation_key="status",
            value_fn=lambda value: value.state_status,
            device_class=SensorDeviceClass.ENUM,
            options=sorted(set(STATE_STATUS_TAGS.values())),
        ),
    ),
    MieleSensorDefinition(
        types=(
            MieleAppliance.WASHING_MACHINE,
            MieleAppliance.WASHING_MACHINE_SEMI_PROFESSIONAL,
            MieleAppliance.TUMBLE_DRYER,
            MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL,
            MieleAppliance.DISHWASHER,
            MieleAppliance.DISH_WARMER,
            MieleAppliance.OVEN,
            MieleAppliance.OVEN_MICROWAVE,
            MieleAppliance.STEAM_OVEN,
            MieleAppliance.MICROWAVE,
            MieleAppliance.ROBOT_VACUUM_CLEANER,
            MieleAppliance.WASHER_DRYER,
            MieleAppliance.STEAM_OVEN_COMBI,
            MieleAppliance.STEAM_OVEN_MICRO,
            MieleAppliance.DIALOG_OVEN,
            MieleAppliance.STEAM_OVEN_MK2,
        ),
        description=MieleSensorDescription(
            key="state_program_id",
            translation_key="program_id",
            device_class=SensorDeviceClass.ENUM,
            value_fn=lambda value: value.state_program_id,
        ),
    ),
    MieleSensorDefinition(
        types=(MieleAppliance.COFFEE_SYSTEM,),
        description=MieleSensorDescription(
            key="state_program_id",
            translation_key="program_id",
            device_class=SensorDeviceClass.ENUM,
            value_fn=lambda value: value.state_program_id,
            extra_attributes={
                ATTRIBUTE_PROFILE: _get_coffee_profile,
            },
        ),
    ),
    MieleSensorDefinition(
        types=(
            MieleAppliance.WASHING_MACHINE,
            MieleAppliance.WASHING_MACHINE_SEMI_PROFESSIONAL,
            MieleAppliance.TUMBLE_DRYER,
            MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL,
            MieleAppliance.DISHWASHER,
            MieleAppliance.DISH_WARMER,
            MieleAppliance.OVEN,
            MieleAppliance.OVEN_MICROWAVE,
            MieleAppliance.STEAM_OVEN,
            MieleAppliance.MICROWAVE,
            MieleAppliance.COFFEE_SYSTEM,
            MieleAppliance.WASHER_DRYER,
            MieleAppliance.STEAM_OVEN_COMBI,
            MieleAppliance.STEAM_OVEN_MICRO,
            MieleAppliance.DIALOG_OVEN,
            MieleAppliance.STEAM_OVEN_MK2,
        ),
        description=MieleSensorDescription(
            key="state_program_phase",
            translation_key="program_phase",
            value_fn=lambda value: value.state_program_phase,
            device_class=SensorDeviceClass.ENUM,
        ),
    ),
    MieleSensorDefinition(
        types=(
            MieleAppliance.WASHING_MACHINE,
            MieleAppliance.WASHING_MACHINE_SEMI_PROFESSIONAL,
            MieleAppliance.TUMBLE_DRYER,
            MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL,
            MieleAppliance.DISHWASHER,
            MieleAppliance.DISH_WARMER,
            MieleAppliance.OVEN,
            MieleAppliance.OVEN_MICROWAVE,
            MieleAppliance.STEAM_OVEN,
            MieleAppliance.MICROWAVE,
            MieleAppliance.ROBOT_VACUUM_CLEANER,
            MieleAppliance.WASHER_DRYER,
            MieleAppliance.STEAM_OVEN_COMBI,
            MieleAppliance.STEAM_OVEN_MICRO,
            MieleAppliance.DIALOG_OVEN,
            MieleAppliance.COFFEE_SYSTEM,
            MieleAppliance.STEAM_OVEN_MK2,
        ),
        description=MieleSensorDescription(
            key="state_program_type",
            translation_key="program_type",
            value_fn=lambda value: StateProgramType(value.state_program_type).name,
            entity_category=EntityCategory.DIAGNOSTIC,
            device_class=SensorDeviceClass.ENUM,
            options=sorted(set(StateProgramType.keys())),
        ),
    ),
    MieleSensorDefinition(
        types=(
            MieleAppliance.WASHING_MACHINE,
            MieleAppliance.WASHING_MACHINE_SEMI_PROFESSIONAL,
            MieleAppliance.TUMBLE_DRYER,
            MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL,
            MieleAppliance.DISHWASHER,
            MieleAppliance.WASHER_DRYER,
        ),
        description=MieleSensorDescription(
            key="current_energy_consumption",
            translation_key="energy_consumption",
            value_fn=lambda value: value.current_energy_consumption,
            device_class=SensorDeviceClass.ENERGY,
            state_class=SensorStateClass.TOTAL_INCREASING,
            native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
            suggested_display_precision=1,
            entity_category=EntityCategory.DIAGNOSTIC,
        ),
    ),
    MieleSensorDefinition(
        types=(
            MieleAppliance.WASHING_MACHINE,
            MieleAppliance.WASHING_MACHINE_SEMI_PROFESSIONAL,
            MieleAppliance.TUMBLE_DRYER,
            MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL,
            MieleAppliance.DISHWASHER,
            MieleAppliance.WASHER_DRYER,
        ),
        description=MieleSensorDescription(
            key="energy_forecast",
            translation_key="energy_forecast",
            value_fn=(
                lambda value: value.energy_forecast * 100
                if value.energy_forecast is not None
                else None
            ),
            native_unit_of_measurement=PERCENTAGE,
            entity_category=EntityCategory.DIAGNOSTIC,
        ),
    ),
    MieleSensorDefinition(
        types=(
            MieleAppliance.WASHING_MACHINE,
            MieleAppliance.DISHWASHER,
            MieleAppliance.WASHER_DRYER,
        ),
        description=MieleSensorDescription(
            key="current_water_consumption",
            translation_key="water_consumption",
            value_fn=lambda value: value.current_water_consumption,
            device_class=SensorDeviceClass.WATER,
            state_class=SensorStateClass.TOTAL_INCREASING,
            native_unit_of_measurement=UnitOfVolume.LITERS,
            suggested_display_precision=0,
            entity_category=EntityCategory.DIAGNOSTIC,
        ),
    ),
    MieleSensorDefinition(
        types=(
            MieleAppliance.WASHING_MACHINE,
            MieleAppliance.DISHWASHER,
            MieleAppliance.WASHER_DRYER,
        ),
        description=MieleSensorDescription(
            key="water_forecast",
            translation_key="water_forecast",
            value_fn=(
                lambda value: value.water_forecast * 100
                if value.water_forecast is not None
                else None
            ),
            native_unit_of_measurement=PERCENTAGE,
            entity_category=EntityCategory.DIAGNOSTIC,
        ),
    ),
    MieleSensorDefinition(
        types=(
            MieleAppliance.WASHING_MACHINE,
            MieleAppliance.WASHING_MACHINE_SEMI_PROFESSIONAL,
            MieleAppliance.WASHER_DRYER,
        ),
        description=MieleSensorDescription(
            key="state_spinning_speed",
            translation_key="spin_speed",
            value_fn=lambda value: value.state_spinning_speed,
            native_unit_of_measurement=REVOLUTIONS_PER_MINUTE,
            entity_category=EntityCategory.DIAGNOSTIC,
        ),
    ),
    MieleSensorDefinition(
        types=(
            MieleAppliance.WASHING_MACHINE,
            MieleAppliance.WASHING_MACHINE_SEMI_PROFESSIONAL,
            MieleAppliance.TUMBLE_DRYER,
            MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL,
            MieleAppliance.DISHWASHER,
            MieleAppliance.OVEN,
            MieleAppliance.OVEN_MICROWAVE,
            MieleAppliance.STEAM_OVEN,
            MieleAppliance.MICROWAVE,
            MieleAppliance.ROBOT_VACUUM_CLEANER,
            MieleAppliance.WASHER_DRYER,
            MieleAppliance.STEAM_OVEN_COMBI,
            MieleAppliance.STEAM_OVEN_MICRO,
            MieleAppliance.DIALOG_OVEN,
            MieleAppliance.STEAM_OVEN_MK2,
        ),
        description=MieleSensorDescription(
            key="state_remaining_time",
            translation_key="remaining_time",
            value_fn=lambda value: _convert_duration(value.state_remaining_time),
            end_value_fn=lambda last_value: 0,
            device_class=SensorDeviceClass.DURATION,
            native_unit_of_measurement=UnitOfTime.MINUTES,
            entity_category=EntityCategory.DIAGNOSTIC,
        ),
    ),
    MieleSensorDefinition(
        types=(
            MieleAppliance.WASHING_MACHINE,
            MieleAppliance.TUMBLE_DRYER,
            MieleAppliance.DISHWASHER,
            MieleAppliance.OVEN,
            MieleAppliance.OVEN_MICROWAVE,
            MieleAppliance.STEAM_OVEN,
            MieleAppliance.MICROWAVE,
            MieleAppliance.WASHER_DRYER,
            MieleAppliance.STEAM_OVEN_COMBI,
            MieleAppliance.STEAM_OVEN_MICRO,
            MieleAppliance.DIALOG_OVEN,
            MieleAppliance.ROBOT_VACUUM_CLEANER,
            MieleAppliance.STEAM_OVEN_MK2,
        ),
        description=MieleSensorDescription(
            key="state_elapsed_time",
            translation_key="elapsed_time",
            value_fn=lambda value: _convert_duration(value.state_elapsed_time),
            end_value_fn=lambda last_value: last_value,
            device_class=SensorDeviceClass.DURATION,
            native_unit_of_measurement=UnitOfTime.MINUTES,
            entity_category=EntityCategory.DIAGNOSTIC,
        ),
    ),
    MieleSensorDefinition(
        types=(
            MieleAppliance.WASHING_MACHINE,
            MieleAppliance.WASHING_MACHINE_SEMI_PROFESSIONAL,
            MieleAppliance.TUMBLE_DRYER,
            MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL,
            MieleAppliance.DISHWASHER,
            MieleAppliance.DISH_WARMER,
            MieleAppliance.OVEN,
            MieleAppliance.OVEN_MICROWAVE,
            MieleAppliance.STEAM_OVEN,
            MieleAppliance.MICROWAVE,
            MieleAppliance.WASHER_DRYER,
            MieleAppliance.STEAM_OVEN_COMBI,
            MieleAppliance.STEAM_OVEN_MICRO,
            MieleAppliance.DIALOG_OVEN,
            MieleAppliance.STEAM_OVEN_MK2,
        ),
        description=MieleSensorDescription(
            key="state_start_time",
            translation_key="start_time",
            value_fn=lambda value: _convert_duration(value.state_start_time),
            end_value_fn=lambda last_value: None,
            native_unit_of_measurement=UnitOfTime.MINUTES,
            device_class=SensorDeviceClass.DURATION,
            entity_category=EntityCategory.DIAGNOSTIC,
            suggested_display_precision=2,
            suggested_unit_of_measurement=UnitOfTime.HOURS,
        ),
    ),
    MieleSensorDefinition(
        types=(
            MieleAppliance.WASHING_MACHINE,
            MieleAppliance.WASHING_MACHINE_SEMI_PROFESSIONAL,
            MieleAppliance.TUMBLE_DRYER,
            MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL,
            MieleAppliance.DISHWASHER,
            MieleAppliance.OVEN,
            MieleAppliance.OVEN_MICROWAVE,
            MieleAppliance.STEAM_OVEN,
            MieleAppliance.MICROWAVE,
            MieleAppliance.ROBOT_VACUUM_CLEANER,
            MieleAppliance.WASHER_DRYER,
            MieleAppliance.STEAM_OVEN_COMBI,
            MieleAppliance.STEAM_OVEN_MICRO,
            MieleAppliance.DIALOG_OVEN,
            MieleAppliance.STEAM_OVEN_MK2,
        ),
        description=MieleSensorDescription(
            key="state_finish_timestamp",
            translation_key="finish",
            value_fn=lambda value: _convert_finish_timestamp(
                value.state_remaining_time, value.state_start_time
            ),
            device_class=SensorDeviceClass.TIMESTAMP,
            entity_category=EntityCategory.DIAGNOSTIC,
        ),
    ),
    MieleSensorDefinition(
        types=(
            MieleAppliance.WASHING_MACHINE,
            MieleAppliance.TUMBLE_DRYER,
            MieleAppliance.DISHWASHER,
            MieleAppliance.OVEN,
            MieleAppliance.OVEN_MICROWAVE,
            MieleAppliance.STEAM_OVEN,
            MieleAppliance.MICROWAVE,
            MieleAppliance.WASHER_DRYER,
            MieleAppliance.STEAM_OVEN_COMBI,
            MieleAppliance.STEAM_OVEN_MICRO,
            MieleAppliance.DIALOG_OVEN,
            MieleAppliance.ROBOT_VACUUM_CLEANER,
            MieleAppliance.STEAM_OVEN_MK2,
        ),
        description=MieleSensorDescription(
            key="state_start_timestamp",
            translation_key="start",
            value_fn=lambda value: _convert_start_timestamp(
                value.state_elapsed_time, value.state_start_time
            ),
            device_class=SensorDeviceClass.TIMESTAMP,
            entity_category=EntityCategory.DIAGNOSTIC,
        ),
    ),
    MieleSensorDefinition(
        types=(
            MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL,
            MieleAppliance.OVEN,
            MieleAppliance.OVEN_MICROWAVE,
            MieleAppliance.DISH_WARMER,
            MieleAppliance.STEAM_OVEN,
            MieleAppliance.MICROWAVE,
            MieleAppliance.FRIDGE,
            MieleAppliance.FREEZER,
            MieleAppliance.FRIDGE_FREEZER,
            MieleAppliance.STEAM_OVEN_COMBI,
            MieleAppliance.WINE_CABINET,
            MieleAppliance.WINE_CONDITIONING_UNIT,
            MieleAppliance.WINE_STORAGE_CONDITIONING_UNIT,
            MieleAppliance.STEAM_OVEN_MICRO,
            MieleAppliance.DIALOG_OVEN,
            MieleAppliance.WINE_CABINET_FREEZER,
            MieleAppliance.STEAM_OVEN_MK2,
        ),
        description=MieleSensorDescription(
            key="state_temperature_1",
            zone=1,
            device_class=SensorDeviceClass.TEMPERATURE,
            native_unit_of_measurement=UnitOfTemperature.CELSIUS,
            state_class=SensorStateClass.MEASUREMENT,
            value_fn=lambda value: _convert_temperature(value.state_temperatures, 0),
        ),
    ),
    MieleSensorDefinition(
        types=(
            MieleAppliance.FRIDGE_FREEZER,
            MieleAppliance.WINE_CABINET,
            MieleAppliance.WINE_CONDITIONING_UNIT,
            MieleAppliance.WINE_STORAGE_CONDITIONING_UNIT,
            MieleAppliance.WINE_CABINET_FREEZER,
        ),
        description=MieleSensorDescription(
            key="state_temperature_2",
            zone=2,
            device_class=SensorDeviceClass.TEMPERATURE,
            translation_key="temperature_zone_2",
            native_unit_of_measurement=UnitOfTemperature.CELSIUS,
            state_class=SensorStateClass.MEASUREMENT,
            value_fn=lambda value: _convert_temperature(value.state_temperatures, 1),
        ),
    ),
    MieleSensorDefinition(
        types=(
            MieleAppliance.WINE_CABINET,
            MieleAppliance.WINE_CONDITIONING_UNIT,
            MieleAppliance.WINE_STORAGE_CONDITIONING_UNIT,
            MieleAppliance.WINE_CABINET_FREEZER,
        ),
        description=MieleSensorDescription(
            key="state_temperature_3",
            zone=3,
            device_class=SensorDeviceClass.TEMPERATURE,
            translation_key="temperature_zone_3",
            native_unit_of_measurement=UnitOfTemperature.CELSIUS,
            state_class=SensorStateClass.MEASUREMENT,
            value_fn=lambda value: _convert_temperature(value.state_temperatures, 2),
        ),
    ),
    MieleSensorDefinition(
        types=(
            MieleAppliance.OVEN,
            MieleAppliance.OVEN_MICROWAVE,
            MieleAppliance.STEAM_OVEN_COMBI,
            MieleAppliance.STEAM_OVEN_MK2,
        ),
        description=MieleSensorDescription(
            key="state_core_target_temperature",
            translation_key="core_target_temperature",
            device_class=SensorDeviceClass.TEMPERATURE,
            native_unit_of_measurement=UnitOfTemperature.CELSIUS,
            state_class=SensorStateClass.MEASUREMENT,
            value_fn=lambda value: _convert_temperature(
                value.state_core_target_temperature, 0
            ),
        ),
    ),
    MieleSensorDefinition(
        types=(
            MieleAppliance.WASHING_MACHINE,
            MieleAppliance.WASHER_DRYER,
            MieleAppliance.OVEN,
            MieleAppliance.OVEN_MICROWAVE,
            MieleAppliance.STEAM_OVEN_MICRO,
            MieleAppliance.STEAM_OVEN_COMBI,
            MieleAppliance.STEAM_OVEN_MK2,
        ),
        description=MieleSensorDescription(
            key="state_target_temperature",
            translation_key="target_temperature",
            zone=1,
            device_class=SensorDeviceClass.TEMPERATURE,
            native_unit_of_measurement=UnitOfTemperature.CELSIUS,
            state_class=SensorStateClass.MEASUREMENT,
            value_fn=lambda value: _convert_temperature(
                value.state_target_temperature, 0
            ),
        ),
    ),
    MieleSensorDefinition(
        types=(
            MieleAppliance.OVEN,
            MieleAppliance.OVEN_MICROWAVE,
            MieleAppliance.STEAM_OVEN_COMBI,
        ),
        description=MieleSensorDescription(
            key="state_core_temperature",
            translation_key="core_temperature",
            device_class=SensorDeviceClass.TEMPERATURE,
            native_unit_of_measurement=UnitOfTemperature.CELSIUS,
            state_class=SensorStateClass.MEASUREMENT,
            value_fn=lambda value: _convert_temperature(
                value.state_core_temperature, 0
            ),
        ),
    ),
    *(
        MieleSensorDefinition(
            types=(
                MieleAppliance.HOB_HIGHLIGHT,
                MieleAppliance.HOB_INDUCT_EXTR,
                MieleAppliance.HOB_INDUCTION,
            ),
            description=MieleSensorDescription(
                key="state_plate_step",
                translation_key="plate",
                translation_placeholders={"plate_no": str(i)},
                zone=i,
                device_class=SensorDeviceClass.ENUM,
                options=sorted(PlatePowerStep.keys()),
                value_fn=lambda value: None,
                unique_id_fn=lambda device_id,
                description: f"{device_id}-{description.key}-{description.zone}",
            ),
        )
        for i in range(1, 7)
    ),
    MieleSensorDefinition(
        types=(
            MieleAppliance.WASHER_DRYER,
            MieleAppliance.TUMBLE_DRYER,
            MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL,
        ),
        description=MieleSensorDescription(
            key="state_drying_step",
            translation_key="drying_step",
            value_fn=lambda value: StateDryingStep(
                cast(int, value.state_drying_step)
            ).name,
            entity_category=EntityCategory.DIAGNOSTIC,
            device_class=SensorDeviceClass.ENUM,
            options=sorted(StateDryingStep.keys()),
        ),
    ),
    MieleSensorDefinition(
        types=(MieleAppliance.ROBOT_VACUUM_CLEANER,),
        description=MieleSensorDescription(
            key="state_battery",
            value_fn=lambda value: value.state_battery_level,
            native_unit_of_measurement=PERCENTAGE,
            entity_category=EntityCategory.DIAGNOSTIC,
            device_class=SensorDeviceClass.BATTERY,
        ),
    ),
)


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: MieleConfigEntry,
    async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
    """Set up the sensor platform."""
    coordinator = config_entry.runtime_data
    added_devices: set[str] = set()  # device_id
    added_entities: set[str] = set()  # unique_id

    def _get_entity_class(definition: MieleSensorDefinition) -> type[MieleSensor]:
        """Get the entity class for the sensor."""
        return {
            "state_status": MieleStatusSensor,
            "state_program_id": MieleProgramIdSensor,
            "state_program_phase": MielePhaseSensor,
            "state_plate_step": MielePlateSensor,
            "state_elapsed_time": MieleTimeSensor,
            "state_remaining_time": MieleTimeSensor,
            "state_start_time": MieleTimeSensor,
            "state_start_timestamp": MieleAbsoluteTimeSensor,
            "state_finish_timestamp": MieleAbsoluteTimeSensor,
            "current_energy_consumption": MieleConsumptionSensor,
            "current_water_consumption": MieleConsumptionSensor,
        }.get(definition.description.key, MieleSensor)

    def _is_entity_registered(unique_id: str) -> bool:
        """Check if the entity is already registered."""
        entity_registry = er.async_get(hass)
        return any(
            entry.platform == DOMAIN and entry.unique_id == unique_id
            for entry in entity_registry.entities.values()
        )

    def _is_sensor_enabled(
        definition: MieleSensorDefinition,
        device: MieleDevice,
        unique_id: str,
    ) -> bool:
        """Check if the sensor is enabled."""
        if (
            definition.description.device_class == SensorDeviceClass.TEMPERATURE
            and definition.description.value_fn(device) is None
            and definition.description.zone != 1
        ):
            # all appliances supporting temperature have at least zone 1, for other zones
            # don't create entity if API signals that datapoint is disabled, unless the sensor
            # already appeared in the past (= it provided a valid value)
            return _is_entity_registered(unique_id)
        if (
            definition.description.key == "state_plate_step"
            and definition.description.zone is not None
            and definition.description.zone > _get_plate_count(device.tech_type)
        ):
            # don't create plate entity if not expected by the appliance tech type
            return False
        return True

    def _async_add_devices() -> None:
        nonlocal added_devices, added_entities
        entities: list = []
        entity_class: type[MieleSensor]
        new_devices_set, current_devices = coordinator.async_add_devices(added_devices)
        added_devices = current_devices

        for device_id, device in coordinator.data.devices.items():
            for definition in SENSOR_TYPES:
                # device is not supported, skip
                if device.device_type not in definition.types:
                    continue

                entity_class = _get_entity_class(definition)
                unique_id = (
                    definition.description.unique_id_fn(
                        device_id, definition.description
                    )
                    if definition.description.unique_id_fn is not None
                    else MieleEntity.get_unique_id(device_id, definition.description)
                )

                # entity was already added, skip
                if device_id not in new_devices_set and unique_id in added_entities:
                    continue

                # sensors is not enabled, skip
                if not _is_sensor_enabled(definition, device, unique_id):
                    continue

                added_entities.add(unique_id)
                entities.append(
                    entity_class(coordinator, device_id, definition.description)
                )
        async_add_entities(entities)

    config_entry.async_on_unload(coordinator.async_add_listener(_async_add_devices))
    _async_add_devices()


APPLIANCE_ICONS = {
    MieleAppliance.WASHING_MACHINE: "mdi:washing-machine",
    MieleAppliance.TUMBLE_DRYER: "mdi:tumble-dryer",
    MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL: "mdi:tumble-dryer",
    MieleAppliance.DISHWASHER: "mdi:dishwasher",
    MieleAppliance.OVEN: "mdi:chef-hat",
    MieleAppliance.OVEN_MICROWAVE: "mdi:chef-hat",
    MieleAppliance.HOB_HIGHLIGHT: "mdi:pot-steam-outline",
    MieleAppliance.STEAM_OVEN: "mdi:chef-hat",
    MieleAppliance.MICROWAVE: "mdi:microwave",
    MieleAppliance.COFFEE_SYSTEM: "mdi:coffee-maker",
    MieleAppliance.HOOD: "mdi:turbine",
    MieleAppliance.FRIDGE: "mdi:fridge-industrial-outline",
    MieleAppliance.FREEZER: "mdi:fridge-industrial-outline",
    MieleAppliance.FRIDGE_FREEZER: "mdi:fridge-outline",
    MieleAppliance.ROBOT_VACUUM_CLEANER: "mdi:robot-vacuum",
    MieleAppliance.WASHER_DRYER: "mdi:washing-machine",
    MieleAppliance.DISH_WARMER: "mdi:heat-wave",
    MieleAppliance.HOB_INDUCTION: "mdi:pot-steam-outline",
    MieleAppliance.STEAM_OVEN_COMBI: "mdi:chef-hat",
    MieleAppliance.WINE_CABINET: "mdi:glass-wine",
    MieleAppliance.WINE_CONDITIONING_UNIT: "mdi:glass-wine",
    MieleAppliance.WINE_STORAGE_CONDITIONING_UNIT: "mdi:glass-wine",
    MieleAppliance.STEAM_OVEN_MICRO: "mdi:chef-hat",
    MieleAppliance.DIALOG_OVEN: "mdi:chef-hat",
    MieleAppliance.WINE_CABINET_FREEZER: "mdi:glass-wine",
    MieleAppliance.HOB_INDUCT_EXTR: "mdi:pot-steam-outline",
}


class MieleSensor(MieleEntity, SensorEntity):
    """Representation of a Sensor."""

    entity_description: MieleSensorDescription

    def __init__(
        self,
        coordinator: MieleDataUpdateCoordinator,
        device_id: str,
        description: MieleSensorDescription,
    ) -> None:
        """Initialize the sensor."""
        super().__init__(coordinator, device_id, description)
        if description.unique_id_fn is not None:
            self._attr_unique_id = description.unique_id_fn(device_id, description)

    @property
    def native_value(self) -> StateType | datetime:
        """Return the state of the sensor."""
        return self.entity_description.value_fn(self.device)

    @property
    def extra_state_attributes(self) -> Mapping[str, Any] | None:
        """Return extra_state_attributes."""
        if self.entity_description.extra_attributes is None:
            return None
        attr = {}
        for key, value in self.entity_description.extra_attributes.items():
            attr[key] = value(self.device)
        return attr


class MieleRestorableSensor(MieleSensor, RestoreSensor):
    """Representation of a Sensor whose internal state can be restored."""

    _attr_native_value: StateType | datetime

    async def async_added_to_hass(self) -> None:
        """When entity is added to hass."""
        await super().async_added_to_hass()

        # recover last value from cache when adding entity
        last_data = await self.async_get_last_sensor_data()
        if last_data:
            self._attr_native_value = last_data.native_value  # type: ignore[assignment]

    @property
    def native_value(self) -> StateType | datetime:
        """Return the state of the sensor.

        It is necessary to override `native_value` to fall back to the default
        attribute-based implementation, instead of the function-based
        implementation in `MieleSensor`.
        """
        return self._attr_native_value

    def _update_native_value(self) -> None:
        """Update the native value attribute of the sensor."""
        self._attr_native_value = self.entity_description.value_fn(self.device)

    @callback
    def _handle_coordinator_update(self) -> None:
        """Handle updated data from the coordinator."""
        self._update_native_value()
        super()._handle_coordinator_update()


class MielePlateSensor(MieleSensor):
    """Representation of a Sensor."""

    entity_description: MieleSensorDescription

    @property
    def native_value(self) -> StateType:
        """Return the state of the plate sensor."""
        # state_plate_step is [] if all zones are off

        return (
            PlatePowerStep(
                cast(
                    int,
                    self.device.state_plate_step[
                        cast(int, self.entity_description.zone) - 1
                    ].value_raw,
                )
            ).name
            if self.device.state_plate_step
            else PlatePowerStep.plate_step_0.name
        )


class MieleStatusSensor(MieleSensor):
    """Representation of the status sensor."""

    def __init__(
        self,
        coordinator: MieleDataUpdateCoordinator,
        device_id: str,
        description: MieleSensorDescription,
    ) -> None:
        """Initialize the sensor."""
        super().__init__(coordinator, device_id, description)
        self._attr_name = None
        self._attr_icon = APPLIANCE_ICONS.get(
            MieleAppliance(self.device.device_type),
            "mdi:state-machine",
        )

    @property
    def native_value(self) -> StateType:
        """Return the state of the sensor."""
        return STATE_STATUS_TAGS.get(StateStatus(self.device.state_status))

    @property
    def available(self) -> bool:
        """Return the availability of the entity."""
        # This sensor should always be available
        return True


# Some phases have names that are not valid python identifiers, so we need to translate
# them in order to avoid breaking changes
PROGRAM_PHASE_TRANSLATION = {
    "second_espresso": "2nd_espresso",
    "second_grinding": "2nd_grinding",
    "second_pre_brewing": "2nd_pre_brewing",
}


class MielePhaseSensor(MieleSensor):
    """Representation of the program phase sensor."""

    @property
    def native_value(self) -> StateType:
        """Return the state of the phase sensor."""
        program_phase = PROGRAM_PHASE[self.device.device_type](
            self.device.state_program_phase
        ).name

        return (
            PROGRAM_PHASE_TRANSLATION.get(program_phase, program_phase)
            if program_phase is not None
            else None
        )

    @property
    def options(self) -> list[str]:
        """Return the options list for the actual device type."""
        phases = PROGRAM_PHASE[self.device.device_type].keys()
        return sorted([PROGRAM_PHASE_TRANSLATION.get(phase, phase) for phase in phases])


class MieleProgramIdSensor(MieleSensor):
    """Representation of the program id sensor."""

    _unrecorded_attributes = frozenset({ATTRIBUTE_PROFILE})

    @property
    def native_value(self) -> StateType:
        """Return the state of the sensor."""
        return (
            PROGRAM_IDS[self.device.device_type](self.device.state_program_id).name
            if self.device.device_type in PROGRAM_IDS
            else None
        )

    @property
    def options(self) -> list[str]:
        """Return the options list for the actual device type."""
        return sorted(PROGRAM_IDS.get(self.device.device_type, {}).keys())


class MieleTimeSensor(MieleRestorableSensor):
    """Representation of time sensors keeping state from cache."""

    def _update_native_value(self) -> None:
        """Update the last value of the sensor."""

        current_value = self.entity_description.value_fn(self.device)
        current_status = StateStatus(self.device.state_status)

        # report end-specific value when program ends (some devices are immediately reporting 0...)
        if (
            current_status == StateStatus.PROGRAM_ENDED
            and self.entity_description.end_value_fn is not None
        ):
            self._attr_native_value = self.entity_description.end_value_fn(
                self._attr_native_value
            )

        # keep value when program ends if no function is specified
        elif current_status == StateStatus.PROGRAM_ENDED:
            pass

        # force unknown when appliance is not working (some devices are keeping last value until a new cycle starts)
        elif current_status in (StateStatus.OFF, StateStatus.ON, StateStatus.IDLE):
            self._attr_native_value = None

        # otherwise, cache value and return it
        else:
            self._attr_native_value = current_value


class MieleAbsoluteTimeSensor(MieleRestorableSensor):
    """Representation of absolute time sensors handling precision correctness."""

    _previous_value: StateType | datetime = None

    def _update_native_value(self) -> None:
        """Update the last value of the sensor."""
        current_value = self.entity_description.value_fn(self.device)
        current_status = StateStatus(self.device.state_status)

        # The API reports with minute precision, to avoid changing
        # the value too often, we keep the cached value if it differs
        # less than 90s from the new value
        if (
            isinstance(self._previous_value, datetime)
            and isinstance(current_value, datetime)
            and (
                self._previous_value - timedelta(seconds=90)
                < current_value
                < self._previous_value + timedelta(seconds=90)
            )
        ) or current_status == StateStatus.PROGRAM_ENDED:
            return

        # force unknown when appliance is not working (some devices are keeping last value until a new cycle starts)
        if current_status in (StateStatus.OFF, StateStatus.ON, StateStatus.IDLE):
            self._attr_native_value = None

        # otherwise, cache value and return it
        else:
            self._attr_native_value = current_value
            self._previous_value = current_value


class MieleConsumptionSensor(MieleRestorableSensor):
    """Representation of consumption sensors keeping state from cache."""

    _is_reporting: bool = False

    def _update_native_value(self) -> None:
        """Update the last value of the sensor."""
        current_value = self.entity_description.value_fn(self.device)
        current_status = StateStatus(self.device.state_status)
        # Guard for corrupt restored value
        restored_value = (
            self._attr_native_value
            if isinstance(self._attr_native_value, (int, float))
            else 0
        )
        last_value = (
            float(cast(str, restored_value))
            if self._attr_native_value is not None
            else 0
        )

        # Force unknown when appliance is not able to report consumption
        if current_status in (
            StateStatus.ON,
            StateStatus.OFF,
            StateStatus.PROGRAMMED,
            StateStatus.WAITING_TO_START,
            StateStatus.IDLE,
            StateStatus.SERVICE,
        ):
            self._is_reporting = False
            self._attr_native_value = None

        # appliance might report the last value for consumption of previous cycle and it will report 0
        # only after a while, so it is necessary to force 0 until we see the 0 value coming from API, unless
        # we already saw a valid value in this cycle from cache
        elif (
            current_status in (StateStatus.IN_USE, StateStatus.PAUSE)
            and not self._is_reporting
            and last_value > 0
        ):
            self._attr_native_value = current_value
            self._is_reporting = True

        elif (
            current_status in (StateStatus.IN_USE, StateStatus.PAUSE)
            and not self._is_reporting
            and current_value is not None
            and cast(int, current_value) > 0
        ):
            self._attr_native_value = 0

        # keep value when program ends
        elif current_status == StateStatus.PROGRAM_ENDED:
            pass

        else:
            self._attr_native_value = current_value
            self._is_reporting = True
